#include "key.h"
#include "cell.h"
#include "table.h"
#include "array.h"
#include "hashmap.h"
#include "quadtree.h"
#include "skipgrid.h"
#include "timer.h"
#include "terrain_file_utils.h"
#include <vector>
#include <iostream>
#include <sstream>
#include <random>
#include <memory>
#include <filesystem>
#include <cmath>
#include <thread>
#include <malloc.h>

using namespace RealScan;

std::vector<Key> generateRandomPoints(std::size_t scale, std::size_t numPoints) 
{
    std::vector<Key> points;
    points.reserve(numPoints);

    std::random_device rd; 
    std::mt19937 gen(rd()); 

    std::uniform_int_distribution<> disX(0, scale - 1);  
    std::uniform_int_distribution<> disY(0, scale - 1);

    for (std::size_t i = 0; i < numPoints; ++i) 
    {
        std::size_t x = disX(gen);
        std::size_t y = disY(gen);
        points.emplace_back(x, y);
    }

    return points;
}

std::vector<std::pair<Key, Key>> generateRandomBlocks(std::size_t scale, std::size_t minWidth, std::size_t maxWidth, std::size_t numBlocks)
{
    std::vector<std::pair<Key, Key>> rects;
    rects.reserve(numBlocks);

    std::random_device rd;
    std::mt19937 gen(rd());
    
    std::uniform_int_distribution<> disX(0, scale - 1);  
    std::uniform_int_distribution<> disY(0, scale - 1); 
    std::uniform_int_distribution<> disW(minWidth, maxWidth);

    for (std::size_t i = 0; i < numBlocks; ++i)
    {
        std::int32_t x = disX(gen);
        std::int32_t y = disY(gen);
        std::int32_t w = disW(gen);

        while ((x + w + 1 > scale) || (y + w + 1 > scale))
        {
            x = disX(gen);
            y = disY(gen);
            w = disW(gen);
        }

        rects.push_back(std::make_pair<Key, Key>({x, y}, {x + w, y + w}));
    }

    return rects;
}

void printProgress(std::size_t current, std::size_t total, int width, const std::string& prefix, const std::string& suffix)
{
    double progress = static_cast<double>(current) / total;

    // prefix
    std::cout << '\r' << prefix << '[';

    // progress bar
    int pos = static_cast<int>(width * progress);
    for (int j = 0; j < width; ++j)
    {
        if (j < pos)
            std::cout << '+';
        else if (j == pos)
            std::cout << '>';
        else
            std::cout << ' ';
    }
    std::cout << ']' << int(progress * 100.0) << '%';

    // suffix
    std::cout << suffix << std::string(60, ' ');
    std::cout.flush(); 
}

void filesWithScale(std::vector<std::string>& files, std::size_t scale, const std::string& path)
{
    // iterate path
    std::vector<std::string> all_files;
    for (const auto& entry : std::filesystem::directory_iterator(path))
    {
        if (entry.is_regular_file())
        {
            all_files.push_back(entry.path().filename().string());
        }
    }

    // find files with scale
    std::copy_if(all_files.begin(), all_files.end(), std::back_inserter(files),
        [&scale](const std::string& str) 
        {
            std::stringstream ss;
            ss << 's' << scale << 'p';
            return str.find(ss.str()) != std::string::npos;
        }
    );
}


int main()
{
    const std::string path{ "../data/" };
    std::vector<std::size_t> scales{ 500, 1000, 5000, 10000, 20000 };

    std::vector<std::string> underlyings
    {
        "array",
        "hashmap",
        "quadtree",
        "skipgrid"
    };

    // type factory
    auto factory = [&underlyings](std::string type, std::size_t scale) -> Table<double>* 
    {
        if (type == underlyings[0]) {
            return (new Array<double>(scale, scale)); 
        } else if (type == underlyings[1]) {
            return (new HashMap<double>());
        } else if (type == underlyings[2]) {
            return (new QuadTree<double>({scale / 2.0, scale / 2.0}, scale));
        } else {
            return (new SkipGrid<double>());
        }
    };

    for (const auto& underlying : underlyings)
    {
        std::cout << std::string(130, '=') << '\n';
        std::cout << "Underlying - " << underlying << '\n';
        std::cout << std::string(130, '=') << '\n';

        /* 
         * results[i] ~ scales[i]
         * 
         * results[i][0] ~ size
         * results[i][0] ~ space
         * results[i][0] ~ block retrieve
         * results[i][0] ~ search
         * results[i][0] ~ insert
         * results[i][0] ~ remove
         */
        std::vector<std::vector<double>> results
        {
            {0, 0, 0, 0, 0, 0},
            {0, 0, 0, 0, 0, 0},
            {0, 0, 0, 0, 0, 0},
            {0, 0, 0, 0, 0, 0},
            {0, 0, 0, 0, 0, 0}
        };

        std::size_t counter = 0;
        std::size_t total   = scales.size() * 15 * 7;
        std::size_t width   = 30;

        for (std::size_t scale_ids = 0; scale_ids < scales.size(); ++scale_ids)
        {
            const auto& scale = scales[scale_ids];

            std::vector<std::string> files;
            filesWithScale(files, scale, path);

            const std::size_t numPoints { 1000 };
            std::vector<Key> searchPts = generateRandomPoints(scale, numPoints / 2);
            std::vector<Key> insertPts = generateRandomPoints(scale, numPoints);
            // std::vector<Key> removePts = generateRandomPoints(scale, numPoints);

            std::size_t minWidth = (scale == 500) ? 100 : 500;
            std::size_t maxWidth = (scale == 500) ? 300 : 1000;
            std::vector<std::pair<Key, Key>> retrieveBlocks = generateRandomBlocks(scale, minWidth, maxWidth, numPoints);

            for (const auto& file : files)
            {
                std::string prefix = std::to_string(scale) + " - " + file + " ";

                auto table = factory(underlying, scale);

                // counter point
                printProgress(++counter, total, width, prefix, " terrain.load");

                TerrainBinary terrain;
                terrain.load(path + file);


                // counter point
                printProgress(++counter, total, width, prefix, "table->insert");

                // table init
                for (std::int32_t x = 0; x < terrain.height(); ++x)
                {
                    for (std::int32_t y = 0; y < terrain.width(); ++y)
                    {
                        if (terrain.get(x, y) == 1)
                        {
                            table->insert({x, y}, 1.0);

                            if (rand() % 3 == 0 && searchPts.size() < numPoints)
                            {
                                searchPts.push_back({x, y});
                            }
                        }
                    }
                    printProgress(counter, total, width, prefix, " table->insert (" + std::to_string(x) + "/" + std::to_string(terrain.height()) + ")");
                }

                // counter point
                printProgress(++counter, total, width, prefix, " size & space");

                // size 
                results[scale_ids][0] += static_cast<double>(table->size());

                // space
                results[scale_ids][1] += static_cast<double>(table->space());

                Timer timer;

                // counter point
                printProgress(++counter, total, width, prefix, " table->toVector");
                
                // block retrieve
                timer.tic();
                for (const auto& block : retrieveBlocks)
                {  
                    std::vector<Cell<double>> vec;
                    table->toVector(vec, block.first, block.second);
                }
                results[scale_ids][2] += timer.toc();

                // counter point
                printProgress(++counter, total, width, prefix, " table->search");

                // search
                timer.tic();
                for (const auto& pt : searchPts)
                {
                    table->search(pt);
                }
                results[scale_ids][3] += timer.toc();

                // counter point
                printProgress(++counter, total, width, prefix, " table->insert");

                // insert
                timer.tic();
                for (const auto& pt : insertPts)
                {
                    table->insert(pt, 1);
                }
                results[scale_ids][4] += timer.toc();

                // counter point
                printProgress(++counter, total, width, prefix, " table->remove");

                // remove
                timer.tic();
                // for (const auto& pt : removePts)
                for (const auto& pt : insertPts)
                {
                    table->remove(pt);
                }
                results[scale_ids][5] += timer.toc();

                delete table;
                table = nullptr;
                malloc_trim(0);
            }

            // avg
            for (std::size_t i = 0; i < results[scale_ids].size(); ++i)
            {
                results[scale_ids][i] /= files.size();
            }
        }
        std::cout << '\n';

        std::cout << "\nEvalution Result:\n";
        // header
        std::cout   << std::setw(15) << "Scale"
                    << std::setw(15) << "Size"
                    << std::setw(15) << "Space" 
                    << std::setw(20) << "Block Retrieve" 
                    << std::setw(20) << "Search" 
                    << std::setw(20) << "Insert" 
                    << std::setw(20) << "Remove" 
                    << '\n';
        // body
        std::cout << std::string(125, '-') << '\n';
        for (std::size_t i = 0; i < scales.size(); ++i)
        {
            std::cout   << std::setw(15) << scales[i]
                        << std::setw(15) << results[i][0]
                        << std::setw(15) << results[i][1]
                        << std::setw(20) << results[i][2]
                        << std::setw(20) << results[i][3]
                        << std::setw(20) << results[i][4]
                        << std::setw(20) << results[i][5]
                        << '\n';
        }
        std::cout << '\n';
    }

    return 0;    
}